﻿using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Red.Core
{
    /// <summary>
    ///     Semaphore to be used with async programming (to avoid reentrancy)
    /// </summary>
    public class AsyncSemaphore
    {
        private readonly static Task s_completed = Task.FromResult( true );
        private readonly Queue<TaskCompletionSource<bool>> _waiters = new Queue<TaskCompletionSource<bool>>();
        private int _currentCount;

        public AsyncSemaphore( int initialCount )
        {
            if ( initialCount < 0 ) throw new ArgumentOutOfRangeException( "initialCount" );
            _currentCount = initialCount;
        }

        public Task WaitAsync()
        {
            lock ( _waiters )
            {
                if ( _currentCount > 0 )
                {
                    --_currentCount;
                    return s_completed;
                }
                else
                {
                    var waiter = new TaskCompletionSource<bool>();
                    _waiters.Enqueue( waiter );
                    return waiter.Task;
                }
            }
        }

        public void Release()
        {
            TaskCompletionSource<bool> toRelease = null;
            lock ( _waiters )
            {
                if ( _waiters.Count > 0 )
                    toRelease = _waiters.Dequeue();
                else
                    ++_currentCount;
            }
            if ( toRelease != null )
                toRelease.SetResult( true );
        }
    }

    /// <summary>
    ///     Lock to be used with async programming (to avoid reentrancy). Sample usage:
    ///     <code>
    ///       private readonly AsyncLock _lock = new AsyncLock();
    ///       // Re-entrant method that can be called from a different place while original instance is still doing async stuff
    ///       public async Task DoStuffAsync()
    ///       {
    ///           using ( var releaser = await _lock.LockAsync() )
    ///           {
    ///               return await ... // long-lasting stuff goes here
    ///           }
    ///       }
    ///     </code>
    /// </summary>
    public class AsyncLock
    {
        private readonly AsyncSemaphore _semaphore;
        private readonly Task<Releaser> _releaser;

        public AsyncLock()
        {
            _semaphore = new AsyncSemaphore( 1 );
            _releaser = Task.FromResult( new Releaser( this ) );
        }

        public Task<Releaser> LockAsync()
        {
            var wait = _semaphore.WaitAsync();
            return wait.IsCompleted ?
                _releaser :
                wait.ContinueWith( ( _, state ) => new Releaser( (AsyncLock)state ),
                    this, CancellationToken.None,
                    TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default );
        }

        public struct Releaser : IDisposable
        {
            private readonly AsyncLock _toRelease;

            internal Releaser( AsyncLock toRelease ) { _toRelease = toRelease; }

            public void Dispose()
            {
                if ( _toRelease != null )
                    _toRelease._semaphore.Release();
            }
        }
    }

}
